package chabernac.gravidroid.model;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class Structure implements iMass {
    private final List<LinearMass> myLinearMasses = new ArrayList<LinearMass>();
    private final List<LinearMass> myInternalLinearMasses = new ArrayList<LinearMass>();
    private Set<Mass> myInternalMasses = null;
    private int myColor;
    private final Vector myCenterPoint = new Vector();
    private final Rectangle myBoxingRect = new Rectangle();

    private final List<Intersection> myIntersections = new ArrayList<Intersection>();

    // TODO value must be calculated some how
    private float myMaxDistancePerTimestep = 10;

    private float[] myPreviousDistances;
    private float[] myDistances;

    private Vector[] myPoints;

    public int getColor() {
        return myColor;
    }

    public void setColor(int aColor) {
        myColor = aColor;
    }

    public void addLinearMass(LinearMass aMass) {
        myLinearMasses.add(aMass);
        aMass.addParent(this);
        initLocalVariables();
    }

    private void initLocalVariables(){
        myDistances = new float[myLinearMasses.size()];   
        myPreviousDistances = new float[myLinearMasses.size()];

        myPoints = new Vector[getLinearMasses().size()];
        for (int i=0;i<getLinearMasses().size();i++){
            myPoints[i] = getLinearMasses().get(i).getMass1().location;
        }
    }
    
    public Vector[] getPoints(){
        return myPoints;
    }

    public void addInteralLinearMass(LinearMass aLinearMass) {
        myInternalLinearMasses.add(aLinearMass);
    }

    @Override
    public void calculateForce(Structure aMass) {
        ForceCalculator.calculateForce(aMass, this);

    }

    @Override
    public void calculateForce(Mass aMass) {
        ForceCalculator.calculateForce(aMass, this);
    }

    @Override
    public void calculateForce(LinearMass aMass) {
        ForceCalculator.calculateForce(aMass, this);

    }

    @Override
    public void calculateForce(LineForce aMass) {
        ForceCalculator.calculateForce(aMass, this);
    }

    @Override
    public void calculateForce(iMass aMass) {
        aMass.calculateForce(this);
    }

    @Override
    public boolean isMoveable() {
        return true;
    }

    public void createInternalLinearMasses(float aCenterMassMass, float aStrength, float aSpeedResistance) {
        Vector theCenterPoint = new Vector();
        theCenterPoint.set(getCenterPoint());

        Set<Mass> theMassesForCenterPoint = new HashSet<Mass>();
        for (LinearMass theLMass : myLinearMasses) {
            float theMoyenDistanceToCenterPoint = (new Vector(theLMass.getMass1().location, theCenterPoint).getLength() + new Vector(
                                                                                                                                     theLMass.getMass2().location, theCenterPoint).getLength()) / 2;
            if (theMoyenDistanceToCenterPoint > theLMass.getVector().getLength() * 3) {
                // we need to insert an extra mass for internal structure stability
                Vector thePointInBetween = new Vector();
                theLMass.getMass1().location.add(theLMass.getMass2().location, thePointInBetween);
                thePointInBetween.divide(2, thePointInBetween);
                Vector theVectorTowardsCenter = new Vector(thePointInBetween, theCenterPoint);
                theVectorTowardsCenter.normalize(theVectorTowardsCenter);
                theVectorTowardsCenter.multiply(theLMass.getVector().getLength(), theVectorTowardsCenter);
                Vector theNewLocation = new Vector();
                thePointInBetween.add(theVectorTowardsCenter, theNewLocation);

                // take one of the masses of the linearmass and copy it
                Mass theNewMass = theLMass.getMass1().copy();
                theNewMass.location = theNewLocation;
                myInternalLinearMasses.add(new LinearMass(theLMass.getMass1(), theNewMass, theLMass.getStrength(), theLMass.getSpeedResistance()));
                myInternalLinearMasses.add(new LinearMass(theLMass.getMass2(), theNewMass, theLMass.getStrength(), theLMass.getSpeedResistance()));
                theMassesForCenterPoint.add(theNewMass);
            }
        }
        Mass theCenterMass = new Mass().setLocation(theCenterPoint).setMass(aCenterMassMass);
        for (Mass theMass : theMassesForCenterPoint) {
            myInternalLinearMasses.add(new LinearMass(theCenterMass, theMass, aStrength, aSpeedResistance));
        }
        myInternalMasses.add(theCenterMass);
    }

    @Override
    public void calculateInternalForces() {
        for (LinearMass theLinearMass : myLinearMasses) {
            theLinearMass.calculateInternalForces();
        }
        for (LinearMass theLinearMass : myInternalLinearMasses) {
            theLinearMass.calculateInternalForces();
        }

    }

    @Override
    public Set<Mass> getInteralMasses() {
        if (myInternalMasses == null) {
            myInternalMasses = new HashSet<Mass>();
            for (LinearMass theLinearMess : myLinearMasses) {
                myInternalMasses.addAll(theLinearMess.getInteralMasses());
            }
        }
        return myInternalMasses;
    }

    public List<LinearMass> getLinearMasses() {
        return myLinearMasses;
    }

    public List<LinearMass> getInternalLinearMasses() {
        return myInternalLinearMasses;
    }

    public Vector getCenterPoint() {
        myCenterPoint.set(0, 0);
        for (Mass theMass : getInteralMasses()) {
            myCenterPoint.add(theMass.location, myCenterPoint);
        }
        myCenterPoint.divide(myInternalMasses.size(), myCenterPoint);
        return myCenterPoint;
    }

    @Override
    public Rectangle getBoxingRect() {

        float minX = myPoints[0].x;
        float minY = myPoints[0].y;
        float maxX = minX;
        float maxY = minY;

        for (Vector theVector : myPoints) {
            if (theVector.x < minX)
                minX = theVector.x;
            if (theVector.y < minY)
                minY = theVector.y;
            if (theVector.x > maxX)
                maxX = theVector.x;
            if (theVector.y > maxY)
                maxY = theVector.y;
        }

        myBoxingRect.topLeftX = minX;
        myBoxingRect.topLeftY = minY;
        myBoxingRect.bottomRightX = maxX;
        myBoxingRect.bottomRightY = maxY;

        return myBoxingRect;
    }

    public List<Intersection> getIntersection(Structure aStructure) {
        myIntersections.clear();
        // if the boxing rects do not intersect then this structures certainly do
        // not collide
        if (!getBoxingRect().intersectsWith(aStructure.getBoxingRect())) {
            return myIntersections;
        }

        // try to detect new intersections
        // run trough all mass points of the structure and test if the other
        // structure contains this mass
        for (Mass theMass : aStructure.getInteralMasses()) {
            List<LinearMass> theIntersection = determineIntersectingLinearMass(theMass);
            theMass.intersectionLinearMass.addAll(theIntersection);
        }

        for (Mass theMass : getInteralMasses()) {
            List<LinearMass> theIntersection = aStructure.determineIntersectingLinearMass(theMass);
            theMass.intersectionLinearMass.addAll(theIntersection);
        }

        // add all the intersections which are still ongoing
        for (Mass theMass : getInteralMasses()) {
            if (theMass.intersectionLinearMass != null) {
                for (LinearMass theLMass : theMass.intersectionLinearMass) {
                    myIntersections.add(new Intersection(theLMass, theMass));
                }
            }
        }

        for (Mass theMass : aStructure.getInteralMasses()) {
            if (theMass.intersectionLinearMass != null) {
                for (LinearMass theLMass : theMass.intersectionLinearMass) {
                    myIntersections.add(new Intersection(theLMass, theMass));
                }
            }
        }

        return myIntersections;
    }

    @Override
    public void resetCollisions() {
        // look at all masses of the structure which where colliding with another
        // structure
        // test is the mass is still in collision with the other structure
        for (Mass theMass : getInteralMasses()) {
            if (theMass.intersectionLinearMass != null) {
                theMass.resetCollisions();
            }
        }
    }

    public Intersection getIntersection(LinearMass aLinearMass) {
        return null;
    }

    public boolean contains(Vector aLocation) {
        return getLinearMassWithNegativeDistanceFor(aLocation) == null;
    }

    public LinearMass getLinearMassWithNegativeDistanceFor(Vector aLocation) {
        for (LinearMass theLinearMass : myLinearMasses) {
            if (theLinearMass.getLine().distanceToPoint(aLocation) < 0) {
                return theLinearMass;
            }
        }
        return null;
    }

    public LinearMass getClosestLinearMassForPrevious(Vector aLocation) {
        float theDistance = myLinearMasses.get(0).getPreviousLine().distanceToPoint(aLocation);
        int theIndex = 0;
        for (int i = 1; i < myLinearMasses.size(); i++) {
            float theNewDistance = myLinearMasses.get(i).getPreviousLine().distanceToPoint(aLocation);
            if (theNewDistance < theDistance) {
                theIndex = i;
                theDistance = theNewDistance;
            }
        }
        return myLinearMasses.get(theIndex);
    }

    private List<LinearMass> determineIntersectingLinearMass(Mass aMass) {
        List<LinearMass> theLMasses = new ArrayList<LinearMass>();
        //float[] theDistances = getDistances(aMass.location);

        boolean isInside = true;
        for (int i = 0; i < myDistances.length; i++) {
            //if one of the points if further then maximal moving distance, we certainly do not have a collision, return
            myDistances[i] = myLinearMasses.get(i).getLine().distanceToPoint(aMass.location);
            if (myDistances[i] < -50) return theLMasses;

            //this point is outside the structure but within the maximal moving distance, we might have had a collision
            if(myDistances[i] < 0) isInside = false;
        }
        
        getPreviousDistances(aMass.previousLocation);

        if(isInside){
            //we have detected that the mass is insed the structure
            //now look for all linear masses we have crossed
            for (int i = 0; i < myPreviousDistances.length; i++) {
                if(myPreviousDistances[i] < 0)  {
                    theLMasses.add(myLinearMasses.get(i));
                }
            }   
            return theLMasses;
        }

        //at this point we know that the mass is outside the structure but within the maximum moving distance during one frame
        //there might have been a collision in case the mass passed 2 borders of the structure during 1 frame and ended outside the structure

        //in that case we must see 2 changes in sign of the distance

        int changesDeteced = 0;
        LinearMass theLinearMass = null;
        for (int i = 0; i < myPreviousDistances.length; i++) {
            if (myPreviousDistances[i] < 0 && myDistances[i] > 0) {
                //a crossing from outside to the inside
                theLinearMass = myLinearMasses.get(i);
                changesDeteced++;
            } else if(myPreviousDistances[i] > 0 && myDistances[i] < 0){
                //a crossing from the inside to the outside
                changesDeteced++;
            }
        }

        if(changesDeteced == 2 && theLinearMass != null){
            myLinearMasses.add(theLinearMass);
        }

        return theLMasses;
    }

    private void getPreviousDistances(Vector aLocation) {
        for (int i = 0; i < myPreviousDistances.length; i++) {
            myPreviousDistances[i] = myLinearMasses.get(i).getPreviousLine().distanceToPoint(aLocation);
        }
    }

    private float[] getDistances(Vector aLocation) {
        for (int i = 0; i < myDistances.length; i++) {
            myDistances[i] = myLinearMasses.get(i).getLine().distanceToPoint(aLocation);
        }
        return myDistances;
    }

    public LinearMass getLinearMassWithNegativeDistanceForPrevious(Vector aLocation) {
        for (LinearMass theLinearMass : myLinearMasses) {
            if (theLinearMass.getPreviousLine().distanceToPoint(aLocation) <= 0) {
                return theLinearMass;
            }
        }

        return null;
    }

    public class Intersection {
        public final LinearMass linearMass;
        public final Mass mass;

        public Intersection(LinearMass aLinearMass, Mass aMass) {
            linearMass = aLinearMass;
            mass = aMass;
        }

        @Override
        public String toString() {
            return mass.hashCode() + " " + linearMass.hashCode();
        }
    }

    @Override
    public Set<iMass> getParents() {
        return null;
    }

    public void printLines() {
        for (LinearMass theLMass : myLinearMasses) {
            System.out.println("Linear mass addes: " + theLMass.getMass1().location + " --> " + theLMass.getMass2().location);
        }
    }

    public void recalculateRestDistances() {
        for (LinearMass theLMass : myLinearMasses) {
            theLMass.recalculateRestDistances();
        }
    }

    public void redefineInternalMasses() {
        myInternalMasses.clear();
        for (LinearMass theLMass : myInternalLinearMasses) {
            myInternalMasses.add(theLMass.getMass1());
            myInternalMasses.add(theLMass.getMass2());
        }
    }
}
